﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Transactions;
using System.Threading;
using Tycho.Runtime;

namespace Tycho.Transactions {
    public class TransactionalObjectManager : IEnlistmentNotification {
        internal TransactionLog TransactionLog { get; set; }
        [ThreadStatic]
        static TransactionalObjectManager _manager;

        private Transaction _transaction;
        List<ITransactionalObject> PreparedObjects;

        public int Attempt { get; private set; }

        /// <summary>
        /// returns all objects used in the transaction, contains objects merely read
        /// and objects modified. This is a superset of ModifiedObjects.
        /// </summary>
        public List<ITransactionalObject> TouchedObjects { get; private set; }

        /// <summary>
        /// Returns all objects modified in the transaction.
        /// </summary>
        public List<ITransactionalObject> ModifiedObjects { get; private set; }

        public static TransactionalObjectManager Manager {
            get {
                if (_manager == null /* || _manager._transaction.TransactionInformation.Status != TransactionStatus.Active */) {
                    _manager = new TransactionalObjectManager ();
                }

                return _manager;
            }
        }

        TransactionalObjectManager () {
            if (Transaction.Current == null) throw new NoTransactionScopeException ();
            Transaction.Current.EnlistVolatile (this, EnlistmentOptions.None);

            _transaction = Transaction.Current;
            TransactionLog = new TransactionLog ();
        }

        ICollection<ITransactionalObject> TransactionableObjects {
            get { return TransactionLog.Objects.Keys; }
        }

        void Rollback () {
            try {
                foreach (ITransactionalObject t in TransactionLog.Objects.Keys) {
                    t.RollBack ();
                }

                if (PreparedObjects != null) {
                    foreach (ITransactionalObject t in PreparedObjects) {
                        Monitor.Exit (t);
                    }
                }
            } finally {
                Dispose ();
            }
        }

        void IEnlistmentNotification.Commit (Enlistment enlistment) {
            try {
                NoteObjectsInvolvedInTransaction ();

                TransactionaliseNewObjects ();

                foreach (ITransactionalObject t in PreparedObjects) {
                    t.Commit ();
                    Monitor.Exit (t);
                }
            } finally {
                Dispose ();
            }

            enlistment.Done ();
        }

        private void NoteObjectsInvolvedInTransaction () {
            TouchedObjects = TransactionLog.Objects.Keys.ToList ();
            ModifiedObjects = TransactionLog.Objects.Where (o => o.Value.Modified).Select (o => o.Key).ToList ();
        }

        void IEnlistmentNotification.InDoubt (Enlistment enlistment) {
            Dispose ();

            enlistment.Done ();
        }

        void IEnlistmentNotification.Prepare (PreparingEnlistment preparingEnlistment) {
            ICollection<ITransactionalObject> transactionalObjects = TransactionLog.Objects.Keys;
            List<ITransactionalObject> locked = new List<ITransactionalObject> (transactionalObjects.Count);
            foreach (ITransactionalObject t in transactionalObjects) {
                if (Monitor.TryEnter (t)) {
                    locked.Add (t);
                    if (t.Prepare ()) {
                        continue;
                    }
                }

                foreach (ITransactionalObject lt in locked) {
                    Monitor.Exit (lt);
                }

                Rollback ();
                preparingEnlistment.ForceRollback ();
                return;
            }

            PreparedObjects = new List<ITransactionalObject> ();
            PreparedObjects.AddRange (locked);
            preparingEnlistment.Prepared ();
        }

        void IEnlistmentNotification.Rollback (Enlistment enlistment) {
            Rollback ();
            enlistment.Done ();
        }

        void Dispose () {
            _manager = null;
        }

        void TransactionaliseNewObjects () {
            HashSet<AnyObject> done = new HashSet<AnyObject> ();

            foreach (ITransactionalObject t in PreparedObjects) {
                foreach (AnyObject reference in t.ObjectReferences) {
                    TransactionaliseNewObjects (reference, done);
                }
            }
        }

        void TransactionaliseNewObjects (AnyObject obj, HashSet<AnyObject> done) {
            if (done.Contains (obj) || obj.IsNull) {
                return;
            }

            done.Add (obj);

            IEnumerable<AnyObject> objectReferences = obj.ObjectReferences;
            if (objectReferences is TransactionalObject) {
                objectReferences = (objectReferences as TransactionalObject).Object;
            }

            foreach (AnyObject reference in objectReferences) {
                ITransactionalObject txo = reference as ITransactionalObject;

                if (txo != null) {
                    if (!txo.IsTransactional) {
                        txo.Transactionalise ();
                    }
                }

                TransactionaliseNewObjects (reference, done);
            }
        }

        public delegate void TransactionDelegate ();

        public static TransactionalObjectManager RunTransaction (TransactionDelegate transactionWork) {
            return RunTransaction (transactionWork, null);
        }

        public static TransactionalObjectManager RunTransaction (TransactionDelegate transactionWork, TransactionDelegate ifTransactionFails) {
            TransactionalObjectManager manager;
            int attempt = 0;

            while (true) {
                attempt++;
                try {
                    using (TransactionScope tx = new TransactionScope ()) {
                        manager = Manager;
                        transactionWork ();
                        tx.Complete ();
                    }
                    break;
                } catch (TransactionAbortedException) {
                    try {
                        if (ifTransactionFails != null) {
                            ifTransactionFails ();
                        }
                    } finally {
                        _manager = null;
                    }
                }
            }

            manager.Attempt = attempt;
            return manager;
        }
    }
}
